package org.xtlgen;

import java.util.ArrayList;

import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Area;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Element;

/**
 * Represents a Boundary around a polygon in the XtlGen system.
 * <p>
 * A Boundary must be specified like this:
 * </p>
 * <pre>
 * &lt;Boundary width="[width]"&gt;
 *   [subshapes]
 * &lt;/Boundary&gt;
 * </pre>
 * or this:
 * <pre>
 * &lt;Boundary width="[width]/&gt;
 * </pre>
 * in the configuration file.
 */
public class Boundary extends ClosedShape {
  /**
   * Construct a Boundary.
   *
   * @param parent  the parent Shape, null allowed
   * @param params  parameters for this Shape as read from the config file
   */
  public Boundary(Shape parent, Element params) {
    super(parent, params);
  }

  /**
   * Create the encapsulated AWT polygon (a GeneralPath).
   *
   * @param params  parameters for this Shape as read from the config file
   *
   * @return A GeneralPath.
   *
   * @throws IllegalArgumentException if the parent object is not a polygon.
   */
  protected java.awt.Shape createAWTShape(Element params) {
    GeneralPath result = null;
    double width;

    // Get the desired width
    NamedNodeMap attributes = params.getAttributes();
    
    if (attributes == null) {
      throw new IllegalArgumentException(
        "Boundary requires the width attribute");
    }
    else {
      try {
        ShapeAttributes shapeAttributes = new ShapeAttributes(attributes);
        width = shapeAttributes.getDouble("width");
      }
      catch(IllegalArgumentException e) {
        throw new IllegalArgumentException("Boundary: " + e.getMessage());
      }
    }
    
    // Obtain the outside vertices from my parent
    CircularList<Polygon.Vertice> outVertices = new CircularList<Polygon.Vertice>();
    int segType = PathIterator.SEG_MOVETO;
    double[] coords = new double[6];
    PathIterator pathIt = getParent().getAWTShape().getPathIterator(null);

    while ( ! pathIt.isDone() ) {
      segType = pathIt.currentSegment(coords);
      if (segType == PathIterator.SEG_QUADTO ||
          segType == PathIterator.SEG_CUBICTO) {
        throw new IllegalArgumentException("Boundaries are only supported on polygons");
      }
      if (segType != PathIterator.SEG_CLOSE) {
        // Only add if different from prev or next points or if first point
        if (outVertices.size() == 0) {
          outVertices.add(new Polygon.Vertice(coords[0], coords[1]));
        }
        else if (  ! ( (outVertices.get(-1).getX() == coords[0]) &&
                       (outVertices.get(-1).getY() == coords[1]) ) &&
                   ! ( (outVertices.get(0).getX() == coords[0]) &&
                       (outVertices.get(0).getY() == coords[1]) ) ) {
          outVertices.add(new Polygon.Vertice(coords[0], coords[1]));
        }

      }
      pathIt.next();
    }

    if (segType != PathIterator.SEG_CLOSE) {
      throw new IllegalArgumentException("Boundaries are only supported on polygons");
    }    

    // Calculate the inside vertices
    Area parentArea = new Area(getParent().getAWTShape());
    Line2D.Double line1;
    Line2D.Double line2;
    
    int numVertices = outVertices.size();
    CircularList<Polygon.Vertice> inVertices = new CircularList<Polygon.Vertice>(numVertices);
      
    for (int i = 0; i<numVertices; i++) {
      line1 = new Line2D.Double(outVertices.get(i-1), outVertices.get(i));
      line2 = new Line2D.Double(outVertices.get(i), outVertices.get(i+1));
      inVertices.add(calcInVertice(line1, line2, width, parentArea));
    }
      
    // Create the shape
    result = new GeneralPath(GeneralPath.WIND_EVEN_ODD,
                             inVertices.size());
    
    result.moveTo((float)inVertices.get(0).getX(), (float)inVertices.get(0).getY());
    
    for (int i=1; i<inVertices.size(); i++) {
      result.lineTo((float)inVertices.get(i).getX(), (float)inVertices.get(i).getY());
    }
    
    result.closePath();

    // Create the polygons for the boundary and add them as my parent's children
    ArrayList<Polygon.Vertice> vertices = new ArrayList<Polygon.Vertice>();
    vertices.add(inVertices.get(-1));
    vertices.add(outVertices.get(-1));
    vertices.add(null);
    vertices.add(null);
    for (int i=0; i<outVertices.size(); i++) {
      vertices.set(2, outVertices.get(i));
      vertices.set(3, inVertices.get(i));
      
      getParent().addChild(new Polygon(getParent(), vertices));

      vertices.set(0, vertices.get(3));
      vertices.set(1, vertices.get(2));
    }
    
    return result;
  }

  /**
   * Make me invisible
   *
   * @param canvas  the target canvas
   */
  protected void drawSelf(Canvas canvas) {
  }

  /**
   * Get the list of child nodes in params.
   *
   * @return The child node list.
   */
  protected NodeList getChildren(Element params) {
    // Vertices are not children, filter out
    NodeList allNodes = params.getChildNodes();
    ParamList children = new ParamList();

    for (int i=0; i<allNodes.getLength(); i++) {
      Node n = allNodes.item(i);
      if (n.getNodeName().compareTo("Vertice") != 0) {
        children.add(n);
      }
    }

    return children;
  }
  
  /*
   * Calculates an internal vertice for the boundary for the two lines given.
   *
   * We obtain 4 candidate points by creating two lines parallel to each
   * of the given lines and finding their intersections:
   *
   *                              A0   A1
   *
   *                 -----------------  <---- Example A
   *                 |               |
   *                 |            A2 | A3
   *                 |               |
   *                 |               |
   *                 |            B0 | B1
   *                 |               |
   *                 |               |------------  <---- Example B
   *                 |                           |
   *                 |            B2   B3        |
   *                 |                           |
   *                 |                           |
   *                 |                           |
   *                 |                           |
   *                 -----------------------------
   *
   * We might have two cases:
   *
   * A) Only one intersection lies *inside* the shape area, therefore
   *    said point is the vertice (A2 in the figure above).
   *
   * B) Only one intersection lies *outside* the shape area, therefore
   *    the vertice is the point opposite to it (B2 in the figure above).
   */
  private static Polygon.Vertice calcInVertice(Line2D.Double line1,
                                              Line2D.Double line2,
                                              double width,
                                              Area clipArea) {
    Rectangle2D bounds = clipArea.getBounds2D();
    ArrayList<Point2D.Double> signs = new ArrayList<Point2D.Double>();
    signs.add(new Point2D.Double( 1.0,  1.0));
    signs.add(new Point2D.Double( 1.0, -1.0));
    signs.add(new Point2D.Double(-1.0, -1.0));
    signs.add(new Point2D.Double(-1.0,  1.0));
    
    CircularList<Polygon.Vertice> candidates = new CircularList<Polygon.Vertice>();

    int lastInIndex = -1;
    int lastOutIndex = -1;
    int inCount = 0;
    int outCount = 0;
    
    for (int i=0; i<4; i++) {
      Polygon.Vertice candidate = 
        getIntersection(makeParallelLine(line1, width * signs.get(i).getX(), bounds),
                        makeParallelLine(line2, width * signs.get(i).getY(), bounds));
      candidates.add(candidate);
      if ((candidate != null) && clipArea.contains(candidate)) {
        lastInIndex = i;
        inCount++;
      }
      else {
        lastOutIndex = i;
        outCount++;
      }
    }

    if (inCount == 1) {
      return candidates.get(lastInIndex);
    }
    else if (outCount == 1) {
      return candidates.get(lastOutIndex+2);
    }

    // Shrink width until we find the index
    for (int k=0; k<100; k++) {
      width /= 10;
      
      for (int i=0; i<4; i++) {
        Polygon.Vertice candidate = 
          getIntersection(makeParallelLine(line1, width * signs.get(i).getX(), bounds),
                          makeParallelLine(line2, width * signs.get(i).getY(), bounds));
        if ((candidate != null) && clipArea.contains(candidate)) {
          lastInIndex = i;
          inCount++;
        }
        else {
          lastOutIndex = i;
          outCount++;
        }
      }
    
      if (inCount == 1) {
        return candidates.get(lastInIndex);
      }
      else if (outCount == 1) {
        return candidates.get(lastOutIndex+2);
      }
    }
    
    throw new IllegalArgumentException("Could not find internal vertice for Boundary!");
  }
                                     
  /*
   * Calculates the intersection point between the two given line segments.
   * Returns null if they do not intersect.
   *
   * Ref: http://mathworld.wolfram.com/Line-LineIntersection.html
   */
  private static Polygon.Vertice getIntersection(Line2D.Double line1, Line2D.Double line2) {
    if (!line1.intersectsLine(line2)) {
        return null;
    }

    double x =
      det(
        det(line1.getP1().getX(), line1.getP1().getY(),
            line1.getP2().getX(), line1.getP2().getY()),
        line1.getP1().getX() - line1.getP2().getX(),
        det(line2.getP1().getX(), line2.getP1().getY(),
            line2.getP2().getX(), line2.getP2().getY()),
        line2.getP1().getX() - line2.getP2().getX()) /
      det(line1.getP1().getX() - line1.getP2().getX(),
          line1.getP1().getY() - line1.getP2().getY(),
          line2.getP1().getX() - line2.getP2().getX(),
          line2.getP1().getY() - line2.getP2().getY());
      
    double y =
      det(det(line1.getP1().getX(), line1.getP1().getY(),
              line1.getP2().getX(), line1.getP2().getY()),
          line1.getP1().getY() - line1.getP2().getY(),
          det(line2.getP1().getX(), line2.getP1().getY(),
              line2.getP2().getX(), line2.getP2().getY()),
          line2.getP1().getY() - line2.getP2().getY()) /
      det(line1.getP1().getX() - line1.getP2().getX(),
          line1.getP1().getY() - line1.getP2().getY(),
          line2.getP1().getX() - line2.getP2().getX(),
          line2.getP1().getY() - line2.getP2().getY());

    return new Polygon.Vertice(x, y);
  }

  /*
   * Calculates the determinant between the two points given.
   *
   * Ref: http://mathworld.wolfram.com/Line-LineIntersection.html
   */
  private static double det(double a, double b, double c, double d) {
    return a * d - b * c;
  }      
  
  /**
   * Returns a line parallel at the given distance to the given line
   * and extends at least to the given bounds.
   *
   * Ref: http://www.teacherschoice.com.au/mathematics_how-to_library.htm#Trigonometry
   */
  private static Line2D.Double makeParallelLine(Line2D.Double line, double distance,
                                 Rectangle2D bounds) {
    Point2D.Double min = new Point2D.Double(bounds.getX()-1000, bounds.getY()-1000);
    Point2D.Double max = new Point2D.Double(bounds.getX() + bounds.getWidth() +1000,
                                            bounds.getY() + bounds.getHeight() +1000); 
    double hSize = line.getP2().getX() - line.getP1().getX();
    double vSize = line.getP2().getY() - line.getP1().getY();
    double xOffset;
    double yOffset;
    
    if (hSize == 0) {
      // Vertical line
      xOffset = distance;
      yOffset = 0.0;
    }
    else if (vSize == 0) {
      // Horizontal line
      xOffset = 0.0;
      yOffset = distance;
    }
    else {
      // Calculate the angle between the line and the vertical axe
      double angle = Math.atan(Math.abs(vSize)/Math.abs(hSize));

      // With the angle and the given distance we can calculate the offsets
      // in the x and y axes we need to move the points to make the two lines
      // parallel at that distance
      xOffset = distance * Math.sin(angle);
      yOffset = distance * Math.sin(Math.toRadians(90 - Math.toDegrees(angle)));
    }

    // Adjust the y offset sign depending on the rule that the x offset
    // always has the same sign as the given distance
    if (line.getP1().getX() > line.getP2().getX()) {
      if (line.getP1().getY() > line.getP2().getY()) {
        yOffset = - yOffset;
      }
    }
    else {
      if (line.getP1().getY() < line.getP2().getY()) {
        yOffset = - yOffset;
      }
    }

    Line2D.Double result =  new Line2D.Double(line.getP1().getX() + xOffset,
                                              line.getP1().getY() + yOffset,
                                              line.getP2().getX() + xOffset,
                                              line.getP2().getY() + yOffset);

    // Extend
    double minMult;
    double maxMult;

    if (hSize == 0) {
      minMult = Math.ceil(Math.abs((result.getP1().getY() - min.getY())/vSize));
      maxMult = Math.ceil(Math.abs((max.getY() - result.getP2().getY())/vSize));
      vSize = Math.abs(vSize);
    }
    else if (vSize == 0) {
      minMult = Math.ceil(Math.abs((result.getP1().getX() - min.getX())/hSize));
      maxMult = Math.ceil(Math.abs((max.getX() - result.getP2().getX())/hSize));
      hSize = Math.abs(hSize);
    }
    else {
      minMult =
        Math.max(
          Math.ceil(Math.abs((result.getP1().getX() - min.getX())/hSize)),
          Math.ceil(Math.abs((result.getP1().getY() - min.getY())/vSize)));
      maxMult =
        Math.max(
          Math.ceil(Math.abs((max.getX() - result.getP2().getX())/hSize)),
          Math.ceil(Math.abs((max.getY() - result.getP2().getY())/vSize)));
      if ((hSize < 0.0) && (vSize < 0.0))
      {
        hSize = Math.abs(hSize);
        vSize = Math.abs(vSize);
      }
    }

    result.setLine(result.getP1().getX() - minMult * hSize,
                   result.getP1().getY() - minMult * vSize,
                   result.getP2().getX() + maxMult * hSize,
                   result.getP2().getY() + maxMult * vSize);

    return result;
  }

  /*
   * Static block of code used to register a ShapeFactory for Boundaries.
   */
  static {
    Shape.registerShapeFactory(
      "Boundary",
      new Shape.ShapeFactory() {
        public Shape makeShape(Shape parent, Element params) {
          return new Boundary(parent, params);
        }
      });
  }
}